Skip to main content

PWM

This chapter explains how to configure PWM on Luckfox Lume and control PWM output using Shell commands, Python, and C.

1. PWM Subsystem

PWM (Pulse Width Modulation) controls the average output by periodically varying the duration of high and low levels. Linux provides a user-space interface through /sys/class/pwm/.

The main PWM parameters are:

  • Period (period): The duration of one complete PWM cycle, in ns.
  • Duty cycle (duty_cycle): The duration of the high level within a cycle, in ns.
  • Polarity (polarity): normal or inversed.
  • Enable (enable): 1 enables output; 0 stops it.

The relationship between frequency and period is:

frequency = 1,000,000,000 / period

2. PWM Control (Shell)

2.1 PWM Pin

Configure PD18, physical pin 28 on the 40-pin header, as PWM2 channel 2 to output control pulses for an SG90 servo.

ItemConfiguration
Physical pinPin 28 on the 40-pin header
SoC pinPD18
Pin multiplexing functionpwm2_2 (Function 5)
Controller / channelpwm2 / channel 2

2.2 Device Tree Configuration

  1. Open the board-level device tree in the SDK:

    device/config/chips/t153/configs/luckfox_lume/linux-5.10-origin/board.dts
  2. Set the multiplexing function of PD18 to pwm2_2:

    &pio {
    lume_pwm2_2_pd18_active: lume-pwm2-2-pd18-active {
    pins = "PD18";
    function = "pwm2_2";
    drive-strength = <10>;
    bias-disable;
    };

    lume_pwm2_2_pd18_sleep: lume-pwm2-2-pd18-sleep {
    pins = "PD18";
    function = "gpio_in";
    bias-pull-down;
    };
    };

    &pwm2 {
    status = "okay";
    };

    &pwm2_2 {
    pinctrl-names = "active", "sleep";
    pinctrl-0 = <&lume_pwm2_2_pd18_active>;
    pinctrl-1 = <&lume_pwm2_2_pd18_sleep>;
    status = "okay";
    };
  3. Compile the device tree and package the image:

    ./build.sh dts
    ./build.sh
    ./build.sh pack

2.3 Viewing PWM Devices

root@luckfox:~# ls -l /sys/class/pwm
total 0
lrwxrwxrwx 1 root root 0 Sep 3 07:03 pwmchip0 -> ../../devices/platform/soc@3000000/20a0000.pwmcs0/pwm/pwmchip0
lrwxrwxrwx 1 root root 0 Sep 3 07:03 pwmchip16 -> ../../devices/platform/soc@3000000/20c0000.pwm2/pwm/pwmchip16
lrwxrwxrwx 1 root root 0 Sep 3 07:03 pwmchip8 -> ../../devices/platform/soc@3000000/20b0000.pwmcs1/pwm/pwmchip8

2.4 Exporting a PWM Channel

echo 2 > /sys/class/pwm/pwmchip16/export
ls /sys/class/pwm/pwmchip16/pwm2/

After a successful export, the following attributes are available:

capture duty_cycle enable period polarity power uevent
  • period: PWM period, in ns.
  • duty_cycle: High-level duration, in ns; must not exceed period.
  • polarity: Output polarity; can be set to normal or inversed.
  • enable: Write 1 to enable output or 0 to stop it.

2.5 Outputting a 50 Hz Signal with a 1.5 ms Positive Pulse Width

After exporting the channel for the first time, if enable, period, and duty_cycle are all 0, set a valid period before configuring the other attributes. This avoids an Invalid argument error caused by period=0:

cd /sys/class/pwm/pwmchip16/pwm2

echo 0 > enable
echo 0 > duty_cycle
echo 20000000 > period
echo normal > polarity
echo 1500000 > duty_cycle
echo 1 > enable

The period is 20,000,000 ns (20 ms), corresponding to 50 Hz. With normal polarity, the high-level duration is 1,500,000 ns (1.5 ms), corresponding to a 7.5% duty cycle for the SG90 center position.

Always ensure that duty_cycle <= period when configuring PWM. After testing, stop and release the channel:

echo 0 > /sys/class/pwm/pwmchip16/pwm2/enable
echo 2 > /sys/class/pwm/pwmchip16/unexport

Stopping PWM does not disconnect the servo's power supply. Turn off the external power supply after testing.

3. PWM Control (Python)

  1. Example program: Use 1.5 ms as the center position and vary the pulse width sinusoidally between 1.1 and 1.9 ms to move the servo back and forth periodically.

    #!/usr/bin/env python3
    import math
    import signal
    import sys
    import time
    from pathlib import Path

    PWM_CHIP = Path("/sys/class/pwm/pwmchip16")
    CHANNEL = 2
    PWM = PWM_CHIP / f"pwm{CHANNEL}"
    PERIOD_NS = 20_000_000
    CENTER_NS = 1_500_000
    RANGE_NS = 400_000
    STEPS = 200
    running = True


    def stop(signum, frame):
    global running
    running = False


    def write_value(path, value):
    with path.open("w") as file:
    file.write(str(value))


    def main():
    exported = False
    configured = False
    enabled = False
    failed = False
    signal.signal(signal.SIGINT, stop)
    signal.signal(signal.SIGTERM, stop)

    try:
    if not PWM.is_dir():
    write_value(PWM_CHIP / "export", CHANNEL)
    exported = True
    time.sleep(0.1)

    if int((PWM / "period").read_text()) > 0:
    write_value(PWM / "enable", 0)
    write_value(PWM / "duty_cycle", 0)
    write_value(PWM / "period", PERIOD_NS)
    configured = True
    write_value(PWM / "polarity", "normal")
    write_value(PWM / "duty_cycle", CENTER_NS)
    if running:
    write_value(PWM / "enable", 1)
    enabled = True
    print("PWM2: PD18 (pin 28), 50 Hz, pulse 1.1-1.9 ms",
    flush=True)
    print("Press Ctrl+C to return to center and stop.", flush=True)

    while running:
    for step in range(STEPS):
    if not running:
    break
    phase = 2.0 * math.pi * step / STEPS
    pulse_ns = int(CENTER_NS + RANGE_NS * math.sin(phase))
    write_value(PWM / "duty_cycle", pulse_ns)
    time.sleep(0.02)
    except (OSError, ValueError) as error:
    print(f"PWM error: {error}", file=sys.stderr)
    failed = True
    finally:
    if enabled and not failed:
    try:
    write_value(PWM / "duty_cycle", CENTER_NS)
    time.sleep(0.3)
    except OSError as error:
    print(f"PWM center error: {error}", file=sys.stderr)
    failed = True

    stopped = True
    if configured:
    try:
    write_value(PWM / "enable", 0)
    except OSError as error:
    print(f"PWM stop error: {error}", file=sys.stderr)
    failed = True
    stopped = False

    if exported and stopped:
    try:
    write_value(PWM_CHIP / "unexport", CHANNEL)
    except OSError as error:
    print(f"PWM unexport error: {error}", file=sys.stderr)
    failed = True

    if not failed:
    print("PWM stopped.")
    return 1 if failed else 0


    if __name__ == "__main__":
    sys.exit(main())
  2. Open the PWM device:

    PWM_CHIP = Path("/sys/class/pwm/pwmchip16")
    CHANNEL = 2
    PWM = PWM_CHIP / f"pwm{CHANNEL}"

    Control channel 2 through sysfs. If the pwm2 directory does not exist, create it by writing to export; otherwise, reuse it.

  3. Configure and output PWM:

    if int((PWM / "period").read_text()) > 0:
    write_value(PWM / "enable", 0)
    write_value(PWM / "duty_cycle", 0)
    write_value(PWM / "period", PERIOD_NS)

    If the existing period is nonzero, disable output and clear the previous pulse width first. If the initial period is 0, set a valid period first to avoid Invalid argument. Then select normal polarity, set an initial pulse width of 1.5 ms, and enable output.

  4. Adjust the duty cycle:

    phase = 2.0 * math.pi * step / STEPS
    pulse_ns = int(CENTER_NS + RANGE_NS * math.sin(phase))
    write_value(PWM / "duty_cycle", pulse_ns)
    time.sleep(0.02)

    The period is fixed at 20 ms (50 Hz). Pulse widths of 1.1 to 1.9 ms correspond to duty cycles of 5.5% to 9.5%. With 200 steps at approximately 20 ms per step, a full back-and-forth cycle takes about 4 seconds.

  5. Run the program:

    python3 PWM.py

    Output:

4. PWM Control (C)

  1. Complete code: Output a 50 Hz control signal through PWM sysfs to move the servo smoothly back and forth along a sine wave.

    #define _POSIX_C_SOURCE 200809L
    #include <errno.h>
    #include <math.h>
    #include <signal.h>
    #include <stdio.h>
    #include <time.h>
    #include <unistd.h>

    #ifndef PWM_CHIP
    #define PWM_CHIP "/sys/class/pwm/pwmchip16"
    #endif
    #define PWM_PATH PWM_CHIP "/pwm2"
    #define CHANNEL "2"
    #define PI 3.14159265358979323846
    #define PERIOD_NS 20000000U
    #define CENTER_NS 1500000U
    #define RANGE_NS 400000U
    #define STEPS 200

    static volatile sig_atomic_t running = 1;

    static void stop(int signum)
    {
    (void)signum;
    running = 0;
    }

    static void delay_ns(long ns)
    {
    struct timespec delay = { .tv_sec = 0, .tv_nsec = ns };
    while (nanosleep(&delay, &delay) < 0 && errno == EINTR) {}
    }

    static int write_value(const char *path, const char *value)
    {
    FILE *file = fopen(path, "w");
    if (!file) {
    perror(path);
    return -1;
    }
    int failed = fprintf(file, "%s", value) < 0;
    if (fflush(file) == EOF)
    failed = 1;
    if (fclose(file) == EOF)
    failed = 1;
    if (failed)
    fprintf(stderr, "PWM write failed: %s\n", path);
    return failed ? -1 : 0;
    }

    static int write_number(const char *path, unsigned int value)
    {
    char text[32];
    snprintf(text, sizeof(text), "%u", value);
    return write_value(path, text);
    }

    static int read_period(unsigned long long *period)
    {
    FILE *file = fopen(PWM_PATH "/period", "r");
    if (!file) {
    perror(PWM_PATH "/period");
    return -1;
    }
    int failed = fscanf(file, "%llu", period) != 1;
    if (fclose(file) == EOF)
    failed = 1;
    if (failed)
    fprintf(stderr, "Cannot read PWM period\n");
    return failed ? -1 : 0;
    }

    int main(void)
    {
    int exported = 0, configured = 0, enabled = 0, failed = 0;
    int stopped = 1;
    unsigned long long old_period;
    struct sigaction action = {0};
    action.sa_handler = stop;
    sigemptyset(&action.sa_mask);
    if (sigaction(SIGINT, &action, NULL) < 0 ||
    sigaction(SIGTERM, &action, NULL) < 0) {
    perror("sigaction");
    return 1;
    }

    if (access(PWM_PATH, F_OK) < 0) {
    if (write_value(PWM_CHIP "/export", CHANNEL) < 0)
    return 1;
    exported = 1;
    delay_ns(100000000L);
    }
    if (read_period(&old_period) < 0)
    goto error;
    if (old_period > 0) {
    if (write_value(PWM_PATH "/enable", "0") < 0 ||
    write_value(PWM_PATH "/duty_cycle", "0") < 0)
    goto error;
    }
    if (write_number(PWM_PATH "/period", PERIOD_NS) < 0)
    goto error;
    configured = 1;
    if (write_value(PWM_PATH "/polarity", "normal") < 0 ||
    write_number(PWM_PATH "/duty_cycle", CENTER_NS) < 0)
    goto error;
    if (running) {
    if (write_value(PWM_PATH "/enable", "1") < 0)
    goto error;
    enabled = 1;
    puts("PWM2: PD18 (pin 28), 50 Hz, pulse 1.1-1.9 ms");
    puts("Press Ctrl+C to return to center and stop.");
    fflush(stdout);
    }

    while (running) {
    for (int step = 0; step < STEPS && running; ++step) {
    double phase = 2.0 * PI * step / STEPS;
    unsigned int pulse_ns =
    (unsigned int)(CENTER_NS + RANGE_NS * sin(phase));
    if (write_number(PWM_PATH "/duty_cycle", pulse_ns) < 0)
    goto error;
    delay_ns(20000000L);
    }
    }
    goto cleanup;

    error:
    failed = 1;
    cleanup:
    if (enabled && !failed) {
    if (write_number(PWM_PATH "/duty_cycle", CENTER_NS) < 0)
    failed = 1;
    else
    delay_ns(300000000L);
    }
    if (configured && write_value(PWM_PATH "/enable", "0") < 0) {
    failed = 1;
    stopped = 0;
    }
    if (exported && stopped &&
    write_value(PWM_CHIP "/unexport", CHANNEL) < 0)
    failed = 1;
    if (!failed)
    puts("PWM stopped.");
    return failed ? 1 : 0;
    }
  2. Export the PWM channel:

    if (access(PWM_PATH, F_OK) < 0) {
    if (write_value(PWM_CHIP "/export", CHANNEL) < 0)
    return 1;
    exported = 1;
    delay_ns(100000000L);
    }

    PWM_CHIP is pwmchip16, CHANNEL is "2", and the channel path is pwmchip16/pwm2. If export fails, the program exits immediately without configuring output.

  3. Set the period and duty cycle:

    if (old_period > 0) {
    if (write_value(PWM_PATH "/enable", "0") < 0 ||
    write_value(PWM_PATH "/duty_cycle", "0") < 0)
    goto error;
    }
    if (write_number(PWM_PATH "/period", PERIOD_NS) < 0)
    goto error;

    Set a 20 ms period, normal polarity, and an initial pulse width of 1.5 ms.

  4. Control PWM output:

    double phase = 2.0 * PI * step / STEPS;
    unsigned int pulse_ns =
    (unsigned int)(CENTER_NS + RANGE_NS * sin(phase));
    if (write_number(PWM_PATH "/duty_cycle", pulse_ns) < 0)
    goto error;
    delay_ns(20000000L);

    Update the pulse width sinusoidally between 1.1 and 1.9 ms, completing a back-and-forth cycle approximately every 4 seconds.

  5. Unexport the PWM channel:

    if (exported && stopped &&
    write_value(PWM_CHIP "/unexport", CHANNEL) < 0)
    failed = 1;

    Release the channel exported by the program only after output has stopped successfully.

  6. Cross-compile using the ARM toolchain from the Lume SDK.

    export PATH=<Luckfox_Lume_SDK>/out/toolchain/gcc-linaro-11.3.1-2022.06-x86_64_arm-linux-gnueabihf/bin:$PATH
    arm-linux-gnueabihf-gcc -std=c11 -O2 -Wall -Wextra PWM.c -o PWM -lm